package com.wh1200.mybatis.xmlless.resolver

import com.wh1200.mybatis.xmlless.enums.Operations
import com.wh1200.mybatis.xmlless.model.ConditionConnector
import com.wh1200.mybatis.xmlless.model.MethodInfo
import com.wh1200.mybatis.xmlless.model.SqlExpression
import com.wh1200.mybatis.xmlless.model.TableName
import com.wh1200.mybatis.xmlless.model.base.IExpressionToken
import com.wh1200.mybatis.xmlless.model.component.ReplacePropertyToColumnVisitor
import com.wh1200.mybatis.xmlless.model.expression.*
import net.sf.jsqlparser.parser.CCJSqlParserUtil
import java.util.*

/**
 * Created by 吴昊 on 2023/12/11.
 */
object ExpressionResolver {

  private val SPLIT_CHARS = listOf('(', ')', ',', ' ', '+', '-', '*', '/', '>', '<')
  private val SPLIT_STRINGS = SPLIT_CHARS.map { it.toString() }

  fun parseExpression(expression: String, methodInfo: MethodInfo): Pair<String, List<TableName>> {
    val exp = CCJSqlParserUtil.parseExpression(expression)
    val visitor = ReplacePropertyToColumnVisitor(methodInfo)
    exp.accept(visitor)
    val middleTables = visitor.getMiddleTables()
    return exp.toString() to middleTables
  }

  fun parseExpression(str: String): SqlExpression {
    return parseExpression(str) { it }
  }

  private fun parseExpressionToStack(expression: String): Stack<IExpressionToken> {
    val stack = Stack<IExpressionToken>()
    var inString = false
    expression.replace("\\s+".toRegex(), " ").toCharArray().forEachIndexed { index, c ->
      when {
        c == '\'' && !inString                                                       -> {
          inString = true
          stack.push(Token(c.toString(), index, index))
        }

        c == '\'' && inString                                                        -> {
          inString = false
          val prev = stack.pop()
          stack.push(StringValue(prev.toSql() + c, prev.start, prev.end + 1))
        }

        inString                                                                     -> {
          val prev = stack.pop()
          stack.push(Token(prev.toSql() + c, prev.start, prev.end + 1))
        }

        stack.isEmpty() || stack.last().toSql() in SPLIT_STRINGS || c in SPLIT_CHARS -> {
          handleSplitters(c, stack, index)
        }

        else                                                                         -> {
          val prev = stack.pop()
          stack.push(Token(prev.toSql() + c, prev.start, prev.end + 1))
        }
      }
    }
    return stack
  }

  private fun handleSplitters(c: Char, stack: Stack<IExpressionToken>, index: Int) {
    when (c) {
      in listOf('+', '-', '*', '/', '>', '<', '=', "!") -> {
        if (stack.lastOrNull() is Operator) {
          val item = stack.pop()
          stack.push(Operator(item.toSql() + c, item.start, item.end + 1))
        } else {
          stack.push(Operator(c.toString(), index, index))
        }
      }

      '('                                          -> {
        if (stack.isNotEmpty() && stack.last() is Token) {
          val item = stack.pop()
          stack.push(FunctionName(item.toSql(), item.start, item.end))
        }
        stack.push(LeftBrace(index))
      }

      ')'                                          -> stack.push(RightBrace(index))
      ','                                          -> stack.push(Comma(index))
      ' '                                          -> stack.push(Whitespace(index))
      else                                         -> {
        stack.push(Token(c + "", index, index))
      }
    }
  }

  private fun parseExpression(expression: String, replacer: (IExpressionToken) -> IExpressionToken): SqlExpression {
    val stack = parseExpressionToStack(expression)
    val list = mutableListOf<IExpressionToken>()
    while (stack.isNotEmpty()) {
      val item = stack.pop()
      when {
        list.lastOrNull() is LeftBrace                                                                          -> {
          if (item is Token) {
            list.add(FunctionName(item.getToken(), item.start, item.end))
          } else {
            list.add(item)
          }
        }

        item is LeftBrace                                                                                       -> {
          val rightBrace = list.last { it is RightBrace && it.leftBrace == null } as RightBrace
          rightBrace.leftBrace = item
          item.rightBrace = rightBrace
          list.add(item)
        }

        item is StringValue || item is Operator
            || item is Comma || item is Whitespace                                                              -> {
          list.add(item)
        }

        item.toSql().lowercase() in listOf("true", "false")                                                     -> {
          list.add(BoolValue(item.toSql().lowercase().toBoolean(), item.start, item.end))
        }

        item.toSql().matches("\\d+".toRegex())                                                                  -> {
          list.add(NumberValue(item.toSql(), item.start, item.end))
        }

        item.toSql().lowercase() == "in" && stack.last { it !is Whitespace }.toSql().lowercase() == "not" -> {
          var lastToken = stack.pop()
          while(lastToken.toSql().lowercase() != "not") {
            lastToken = stack.pop()
          }
          list.add(Operator("not in", lastToken.start, item.end))
        }
        item.toSql().lowercase() == "any" && stack.last { it !is Whitespace }.toSql().lowercase() == "contains" -> {
          var lastToken = stack.pop()
          while(lastToken.toSql().lowercase() != "contains") {
            lastToken = stack.pop()
          }
          list.add(Operator("contains any", lastToken.start, item.end))
        }

        item.toSql().lowercase() in listOf("between", "ilike", "in", "like", "contains", "is", "null", "not")   -> {
          list.add(Operator(item.toSql(), item.start, item.end))
        }

        item.toSql().lowercase() == "or"                                                                        -> {
          list.add(Connector(ConditionConnector.OR, item.start, item.end))
        }

        item.toSql().lowercase() == "and"                                                                       -> {
          if (isNotInBetween(stack)) {
            list.add(Connector(ConditionConnector.AND, item.start, item.end))
          } else {
            list.add(Operator(item.toSql(), item.start, item.end))
          }
        }

        else                                                                                                    -> {
          list.add(replacer(item))
        }
      }
    }
    return SqlExpression().addAll(list.reversed())
  }

  private fun isNotInBetween(stack: Stack<IExpressionToken>): Boolean {
    val list = stack.filter { it !is Whitespace }
    return list.size < 2 || list[list.size - 2].toSql().uppercase() != Operations.Between.operator
  }

}
